App.require(["htmlTool", "canvasTool", "relativeTool"], function (htmlTool, canvasTool, relativeTool) {
    var app = App.getCurrent();

    app.defineLayer("str-relative-chart", {
        render: function (next) {
            this.initProperty();
            this.data.isChange = true;
            this.data.isMouseIn = false;
            this.fetch("./index.html", function (text) {
                next(this.renderHTML(text));
            })
        },
        removeEdge: function (edge) {
            var index;
            if (typeof edge == "number") {
                index = this._getEdgeById(edge);
            }
            else index = edge;
            this.data.edges.splice(index, 1);
            this.data.isChange = true;
            this.addChange();
        },
        appendEdge: function (edge) {
            this.data.edges.push(edge);
            this.data.isChange = true;
            this.addChange();
        },
        getEdgeByDoubleId: function (from, to) {
            var edges = this.data.edges;
            for (var i = 0; i < edges.length; i++) {
                var e = edges[i];
                if (e.from == from && e.to == to) return e;
            }
            return null;
        },
        getEdgeByIds: function (ids) {
            var result = { edges: [], nodes: [] };
            for (var i = 0; i < ids.length - 1; i++) {
                var from = ids[i], to = ids[i + 1];
                var node = this._getNodeById(from, to), edge = this.getEdgeByDoubleId(from, to);
                if (node && edge) {
                    result.nodes.push(node);
                    result.edges.push(edge);
                }
                else return null;
            }
            return result;
        },
        removeNode: function (node) {
            var index;
            if (typeof node == "number") {
                index = node;
                node = this.data.nodes[index];
            }
            else index = this._getNodeIndex(node);
            this.data.nodes.splice(index, 1); 
            this.data.isChange = true;
            this.data.kp.removeNode(node);
            this._removeEdgesByNode(node);
            var property = this.property;
            if (property.animation) this.addPropertyAnimation(property.animation, this);
            else this.addChange();
        },
        refreshPointsExcept: function (points, id) {
            var nodes = this.data.nodes, originMap = {}, targetMap = {};
            for (var i = 0; i < nodes.length; i++) {
                originMap[nodes[i].id] = nodes[i];
            }
            for (var i = 0; i < points.length; i++) {
                targetMap[points[i].id] = points[i];
            }
            for (var key in originMap) {
                if (key !== id && !targetMap[key]) {
                    this.removeNode(originMap[key]);
                    continue;
                }
                var originNode = originMap[key], targetNode = targetMap[key];
                for (var kk in targetNode) {
                    if (kk !== "x" && kk !== "y") originNode[kk] = targetNode[kk];
                }
            }
            for (var key in targetMap) {
                if (!originMap[key]) this.appendNode(targetMap[key]);
            }
            this.addChange();
        },
        clearNodesExcept: function (id) {
            var nodes = this.data.nodes;
            for (var i = nodes.length - 1; i >= 0; i--) {
                if (nodes[i].id !== id) this.removeNode(nodes[i]);
            }
        },
        appendNode: function (node) {
            node.vector = { x: 0, y: 0};
            this.data.nodes.push(node);
            this.data.kp.addNode(node);
            this.data.isChange = true;
            var property = this.property;
            if (property.animation) this.addPropertyAnimation(property.animation, this);
            else this.addChange();
        },
        changeNode: function (node, optList) {
            if (typeof node == "number") node = this.data.nodes[node];
            if (!node) return;
            this.data.isChange = true;
            if (Array.isArray(optList)) {
                for (var i = 0; i < optList.length; i++) {
                    this.addChange(optList[i].option, node, optList[i].animation);
                }
            }
            else {
                this.addChange(optList.option, node, optList.animation);
            }
        },
        changeEdge: function (edge, optList) {
            if (typeof edge == "number") edge = this.data.edges[edge];
            if (!edge) return;
            this.data.isChange = true;
            if (Array.isArray(optList)) {
                for (var i = 0; i < optList.length; i++) {
                    this.addChange(optList[i].option, edge, optList[i].animation);
                }
            }
            else {
                this.addChange(optList.option, edge, optList.animation);
            }
        },
        getLength: function (edge) {
            var fNode = this._getNodeById(edge.from), tNode = this._getNodeById(edge.to);
            var dx = fNode.x - tNode.x, dy = fNode.y - tNode.y;
            return Math.sqrt(dx * dx + dy * dy);
        },
        _getNodeById: function (id) {
            var nodes = this.data.nodes;
            for (var i = 0; i < nodes.length; i++) {
                if (nodes[i].id == id) return nodes[i];
            }
        },
        _getNodeIndex: function (node) {
            var nodes = this.data.nodes;
            for (var i = 0; i < nodes.length; i++) {
                if (nodes[i] == node) return i;
            }
        },
        _getEdgeById: function (edge) {
            var edges = this.data.edges, id = edge.id;
            for (var i = 0; i < edges.length; i++) {
                if (edges[i].id === id) return i;
            }
        },
        initProperty: function () {
            this.data.attributeStr = this._createPropertyStr(this.property);
        },
        beforeInit: function (next) {
            var property = this.property;
            this.data.edges = htmlTool.getOptions(this.getSlotByName("edges").content);
            this.data.nodes = htmlTool.getOptions(this.getSlotByName("nodes").content);
            this.data.kp = new relativeTool.KP(this.data.nodes, this.data.edges, this.width, this.height, {
                constant: this.constant,
                edgelength: this.edgelength,
                hk: this.hk,
                calcposition: this.calcposition
            });
            
            if (property.animation) this.addPropertyAnimation(property.animation, this);
            next();
        },
        resetAllEdgeLength: function () {
            this.data.kp.resetAllEdgeLength();
        },
        setCustomEdgeLengthByNode: function (node) {
            this.data.kp.setCustomEdgeLengthByNode(node);
        },
        _removeEdgesByNode: function (node) {
            var edges = this.data.edges, id = node.id;
            for (var j = edges.length - 1; j >= 0; j--) {
                var edge = edges[j];
                if (edge.from == id || edge.to == id) {
                    edges.splice(j, 1);
                }
            }
        },
        getEdgesByNode: function (node) {
            var result = [];
            var edges = this.data.edges, id = node.id;
            for (var j = edges.length - 1; j >= 0; j--) {
                var edge = edges[j];
                if (edge.from == id || edge.to == id) {
                    result.push(edge);
                }
            }
            return result;
        },
        moveLayerHandler: function (ev, ctx, ratio) {
            var obj = this.containsBounds(ctx, ev.clientX, ev.clientY, ratio);
            if (obj) {
                if (!this.data.isMouseIn) {
                    this.data.isMouseIn = true;
                    this.data.tempIndex = obj.index;
                    this.data.tempOption = obj.node;
                    this.dispatchCustomEvent("ralativenodemouseenter", {
                        component: this,
                        event: ev,
                        index: obj.index,
                        node: obj.node
                    });
                }
            } else {
                if (this.data.isMouseIn) {
                    this.data.isMouseIn = false;
                    this.dispatchCustomEvent("ralativenodemouseleave", {
                        component: this,
                        event: ev,
                        index: this.data.tempIndex,
                        node: this.data.tempOption
                    });
                    this.data.tempOption = null;
                    this.data.tempIndex = null;
                }
            }
        },
        clickLayerHandler: function (ev, ctx, ratio) {
            var obj = this.containsBounds(ctx, ev.clientX, ev.clientY, ratio);
            if (obj) {
                this.dispatchCustomEvent("ralativenodeclick", {
                    component: this,
                    event: ev,
                    index: obj.index,
                    node: obj.node
                });
            }
        },
        mouseDownLayerHandler: function(ev, ctx, ratio) {
            var obj = this.containsBounds(ctx, ev.clientX, ev.clientY, ratio);
            if (obj) {
                this.dispatchCustomEvent("relativenodemousedown", {
                    component: this,
                    event: ev,
                    index: obj.index,
                    node: obj.node
                });
            }
        },
        containsBounds: function (ctx, x, y, ratio) {
            ctx.save();
            var nodes = this.data.nodes,
                isIn = false;
            this.transformView(ctx, ratio);

            for (var i = 0; i < nodes.length; i++) {
                var node = nodes[i];
                var marker = this.getMarkerByStr(node.marker || this.marker) || this.getDefaultMarker(ratio);
                ctx.save();
                ctx.translate(node.x * ratio + marker.offsetX, node.y * ratio + marker.offsetY);
                isIn = marker.isPointInPath(ctx, x, y, ratio, node);
                ctx.restore();
                if (isIn) break;
            }
            ctx.restore();
            if (i < nodes.length) {
                return {
                    index: i,
                    node: nodes[i]
                }
            }
            return null;
        },
        drawSelf: function (ctx, ratio) {
            var property = this.property;
            if (typeof this.opacity !== "undefined") ctx.globalAlpha = parseFloat(this.opacity);
            var isChange = this.data.kp.computedPosition();
            this.transformView(ctx, ratio);
            var keyToCanvas = canvasTool.keyToCanvas;
            for (var key in property) {
                var ctxKey = keyToCanvas[key];
                if (ctxKey) {
                    if (ctxKey == "fillStyle" || ctxKey == "strokeStyle") ctx[ctxKey] = this.getFillOrStrokeStyle(this[key]);
                    else ctx[ctxKey] = this[key];
                }
            }

            if ("linedash" in property) {
                ctx.setLineDash(this.linedash.split(/\s+/).map(function (item) {
                    return parseFloat(item) * ratio;
                }))
            }
            if (this.shadow) {
                var obj = canvasTool.changeShadowStr(this.shadow, ratio);
                if (obj) {
                    ctx.shadowOffsetX = obj.x;
                    ctx.shadowOffsetY = obj.y;
                    ctx.shadowBlur = obj.blur;
                    ctx.shadowColor = obj.color;
                }
            }

            var nodes = this.data.nodes, edges = this.data.edges, kp = this.data.kp;
            for (var i = 0; i < edges.length; i++) {
                ctx.save();
                var edge = edges[i];
                if ("stroke" in edge) ctx.strokeStyle = this.getFillOrStrokeStyle(edge.stroke);
                if ("linewidth" in edge) ctx.lineWidth = parseFloat(edge.linewidth);
                if ("linedash" in edge) ctx.setLineDash(edge.linedash.split(/\s+/).map(function (item) {
                    return parseFloat(item) * ratio;
                }));
                
                var fNode = kp.nodeMap[edge.from], tNode = kp.nodeMap[edge.to];
                var strokeObj = this.drawEdge(ctx, { type: edge.type || "line", param: edge.param }, fNode, tNode, ratio);
                ctx.restore();
                if (edge.startmarker) {
                    var locObj = strokeObj(edge.startmarkeroffset ? parseFloat(edge.startmarkeroffset) : 0.1);
                    var marker = this.getMarkerByStr(edge.startmarker);
                    ctx.save();
                    ctx.translate(locObj.x, locObj.y);
                    if (marker.rotate === "auto") ctx.rotate(locObj.theta);
                    ctx.translate(marker.offsetX, marker.offsetY);
                    marker.drawToCanvas(ctx, ratio);
                    ctx.restore();
                }

                if (edge.endmarker) {
                    var locObj = strokeObj(1 - (edge.endmarkeroffset ? parseFloat(edge.endmarkeroffset) : 0.1));
                    var marker = this.getMarkerByStr(edge.endmarker);
                    ctx.save();
                    ctx.translate(locObj.x, locObj.y);
                    if (marker.rotate === "auto") ctx.rotate(locObj.theta);
                    ctx.translate(marker.offsetX, marker.offsetY);
                    marker.drawToCanvas(ctx, ratio);
                    ctx.restore();
                }
            }

            for (var i = 0; i < nodes.length; i++) {
                ctx.save();
                var node = nodes[i];
                var marker = this.getMarkerByStr(node.marker || this.marker) || this.getDefaultMarker(ratio);
                ctx.translate(node.x * ratio + marker.offsetX, node.y * ratio + marker.offsetY);
                var obj = { x: node.x * ratio, y: node.y * ratio };
                for (var key in node) {
                    obj[key] = node[key];
                }
                marker.drawToCanvas(ctx, ratio, obj);
                if (typeof node.showcontent !== "undefined") {
                    ctx.fillStyle = this.getFillOrStrokeStyle(node.cfill || "black");
                    ctx.textAlign = "center";
                    ctx.verticalAlign = "middle";
                    ctx.fillText(node.content, marker.width / 2,  marker.height + 15)
                }
                ctx.restore();
            }

            if (!isChange) this.dispatchCustomEvent("relativechangeend", { component: this })
            return isChange;
        },
        drawEdge: function (ctx, obj, fNode, tNode, ratio) {
            switch (obj.type) {
                case "line": return this._drawLine(ctx, fNode, tNode, ratio); 
                case "q-curve": return this._drawQCurve(ctx, fNode, tNode, obj.param, ratio); 
                case "curve": return this._drawCurve(ctx, fNode, tNode, obj.param, ratio); 
                case "self-line": return this._drawSelfLine(ctx, fNode, tNode, obj.param, ratio); 
            }
        },
        _drawSelfLine: function (ctx, fNode, tNode, param, ratio) {
            var sx = fNode.x * ratio, sy = fNode.y * ratio, lx = tNode.x * ratio, ly = tNode.y * ratio;
            var mids = param.split(/\s+/).map(function (item) { return parseFloat(item); });
            ctx.moveTo(sx, sy);
            for (var i = 0; i < mids.length; i += 2) {
                ctx.lineTo(mids[i], mids[i + 1]);
            }
            ctx.lineTo(lx, ly);
            ctx.stroke();
            return function (offset) {
                var points = [{x: sx, y: sy}];
                for (var i = 0; i < mids.length; i += 2) {
                    points.push({x: mids[i], y: mids[i + 1]});
                }
                points.push({ x: lx, y: ly});
                var lens = [], sum = 0;
                for (var i = 0; i < points.length - 1; i++) {
                    var deltaX = points[i + 1].x - points[i].x, deltaY = points[i + 1].y - points[i].y;
                    var len = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
                    sum += len;
                    lens.push(sum);
                }
                lens = lens.map(function (item) { return item / sum });
                for (var i = 0; i < lens.length; i++) {
                    if (offset <= lens[i]) {
                        var cOffset = i == 0 ? offset / lens[0] : (offset - lens[i - 1]) / (lens[i] - lens[i - 1]);
                        var fp = points[i], np = points[i + 1];
                        return {
                            x: (1 - cOffset) * fp.x + cOffset * np.x,
                            y: (1 - cOffset) * fp.y + cOffset * np.y,
                            theta: Math.atan2(np.y - fp.y, np.x - fp.x)
                        }
                    }
                }
            }
        },
        _drawLine: function (ctx, fNode, tNode, ratio) {
            var x1 = fNode.x * ratio, y1 = fNode.y * ratio, x2 = tNode.x * ratio, y2 = tNode.y * ratio;
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
            return function (offset) {
                return {
                    x: (1 - offset) * x1 + offset * x2,
                    y: (1 - offset) * y1 + offset * y2,
                    theta: Math.atan2(y2 - y1, x2 - x1)
                }
            };
        },
        _drawQCurve: function (ctx, fNode, tNode, param, ratio) {
            var sx = fNode.x * ratio, sy = fNode.y * ratio, lx = tNode.x * ratio, ly = tNode.y * ratio;
            var nums = param.split(/\s+/).map(function (item) { return parseFloat(item) });
            var cObj = this._getRelativePoint(sx, sy, lx, ly, nums[0], nums[1]);
            ctx.beginPath();
            ctx.moveTo(sx, sy);
            ctx.quadraticCurveTo(cObj.x, cObj.y, lx, ly);
            ctx.stroke();
            return function (offset) {
                var t1x = (1 - offset) * sx + offset * cObj.x,
                    t1y = (1 - offset) * sy + offset * cObj.y,
                    t2x = (1 - offset) * cObj.x + offset * lx,
                    t2y = (1 - offset) * cObj.y + offset * ly;
                return {
                    x: (1 - offset) * t1x + offset * t2x,
                    y: (1 - offset) * t1y + offset * t2y,
                    theta: Math.atan2(t2y = t1y, t2x - t1x)
                }
            }
        },
        _drawCurve: function (ctx, fNode, tNode, param, ratio) {
            var sx = fNode.x * ratio, sy = fNode.y * ratio, lx = tNode.x * ratio, ly = tNode.y * ratio;
            var nums = param.split(/\s+/).map(function (item) { return parseFloat(item) });
            var c1Obj = this._getRelativePoint(sx, sy, lx, ly, nums[0], nums[1]),
                c2Obj = this._getRelativePoint(sx, sy, lx, ly, nums[2], nums[3]);
            ctx.beginPath();
            ctx.moveTo(sx, sy);
            ctx.bezierCurveTo(c1Obj.x, c1Obj.y, c2Obj.x, c2Obj.y, lx, ly);
            ctx.stroke();
            return function (offset) {
                var t1x = (1 - offset) * sx + offset * c1Obj.x,
                    t1y = (1 - offset) * sy + offset * c1Obj.y,
                    t2x = (1 - offset) * c1Obj.x + offset * c2Obj.x,
                    t2y = (1 - offset) * c1Obj.y + offset * c2Obj.y,
                    t3x = (1 - offset) * c2Obj.x + offset * lx,
                    t3y = (1 - offset) * c2Obj.y + offset * ly,
                    t12x = (1 - offset) * t1x + offset * t2x,
                    t12y = (1 - offset) * t1y + offset * t2y,
                    t23x = (1 - offset) * t2x + offset * t3x,
                    t23y = (1 - offset) * t2y + offset * t3y;
                return {
                    x: (1 - offset) * t12x + offset * t23x,
                    y: (1 - offset) * t12y + offset * t23y,
                    theta: Math.atan2(t23y - t12y, t23x - t12x)
                }
            }
        },
        _getRelativePoint: function (sx, sy, lx, ly, nx, ny) {
            var q1x = (1 - nx) * sx + nx * lx,
                q1y = (1 - nx) * sy + nx * ly;
            var q2x = (sy - ly) * ny / 2, q2y = (lx - sx) * ny / 2;
            return { x: q1x + q2x, y: q1y + q2y };
        },
        transformView: function (ctx, ratio) {
            ctx.translate(this.x * ratio, this.y * ratio);
            if (this.transform) {
                var tList = canvasTool.createTransformList(this.transform, ratio);
                for (var i = 0; i < tList.length; i++) {
                    var tObj = tList[i];
                    ctx[tObj.action].apply(ctx, tObj.args);
                }
            }
        },
        getDefaultMarker: function (ratio) {
            var that = this;
            return {
                width: 20 * ratio,
                height: 20 * ratio,
                offsetX: -10 * ratio,
                offsetY: -10 * ratio,
                isPointInPath: function (ctx, x, y, ratio, obj) {
                    var r = typeof obj.r === "undefined" ? 8 : parseFloat(obj.r);
                    ctx.beginPath();
                    ctx.moveTo(10 * ratio, 10 * ratio);
                    ctx.arc(10 * ratio, 10 * ratio, r * ratio, 0, Math.PI * 2, false);
                    return ctx.isPointInPath(x * ratio, y * ratio);
                },
                drawToCanvas: function (ctx, ratio, obj) {
                    obj = obj || {};
                    var r = typeof obj.r === "undefined" ? 8 : parseFloat(obj.r);
                    ctx.beginPath();
                    ctx.arc(10 * ratio, 10 * ratio, r * ratio, 0, Math.PI * 2, false);
                    ctx.fillStyle = obj.fill ? that.getFillOrStrokeStyle(obj.fill) : "red";
                    ctx.fill();
                    if (obj.stroke) {
                        ctx.strokeStyle = that.getFillOrStrokeStyle(obj.stroke);
                        ctx.stroke();
                    }
                }
            }
        },
        get calcposition() {
            return this.getValue("calcposition");
        },
        set calcposition(value) {
            this.setValue("calcposition", value);
        },
        get constant() {
            return this.getValue("constant");
        },
        set constant(value) {
            this.setValue("constant", value);
        },
        get hk() {
            return this.getValue("hk");
        },
        set hk(value) {
            this.setValue("hk", value);
        },
        get edgelength() {
            return this.getValue("edgelength");
        },
        set edgelength(value) {
            this.setValue("edgelength", value);
        },
        get width() {
            return this.getValue("width");
        },
        set width(value) {
            this.setValue("width", value);
        },
        get height() {
            return this.getValue("height");
        },
        set height(value) {
            this.setValue("height", value);
        },
        get x() {
            return this.getValue("x");
        },
        set x(value) {
            this.setValue("x", value);
        },
        get y() {
            return this.getValue("y");
        },
        set y(value) {
            this.setValue("y", value);
        },
        get transform() {
            return this.getValue("transform");
        },
        set transform(value) {
            this.setValue("transform", value);
        },
        get fill() {
            return this.getValue("fill");
        },
        set fill(value) {
            this.setValue("fill", value);
        },
        get stroke() {
            return this.getValue("stroke");
        },
        set stroke(value) {
            this.setValue("stroke", value);
        },
        get shadow() {
            return this.getValue("shadow");
        },
        set shadow(value) {
            this.setValue("shadow", value);
        },
        set hidden(value) {
            this.setValue("hidden", value);
        },
        get hidden() {
            return this.getValue("hidden");
        },
        get animation() {
            return this.getValue("animation");
        },
        set animation(value) {
            if (this.setValue("animation", value)) {
                if (value) this.addPropertyAnimation(value, this);
            }
        },
        set transition(value) {
            this.setValue("transition", value);
        },
        get transition() {
            return this.getValue("transition");
        },
        set linedash(value) {
            this.setValue("linedash", value);
        },
        get linedash() {
            return this.getValue("linedash");
        },
        set linedashoffset(value) {
            this.setValue("linedashoffset", value);
        },
        get linedashoffset() {
            return this.getValue("linedashoffset");
        },
        set linecap(value) {
            this.setValue("linecap", value);
        },
        get linecap() {
            return this.getValue("linecap");
        },
        set linejoin(value) {
            this.setValue("linejoin", value);
        },
        get linejoin() {
            return this.getValue("linejoin");
        },
        set linewidth(value) {
            this.setValue("linewidth", value);
        },
        get linewidth() {
            return this.getValue("linewidth");
        },
        set miterlimit(value) {
            this.setValue("miterlimit", value);
        },
        get miterlimit() {
            return this.getValue("miterlimit");
        },
        set opacity(value) {
            this.setValue("opacity", value);
        },
        get opacity() {
            return this.getValue("opacity");
        }
    });
});